# Time Flies
# Author: Jerry-programming
# Latest update: 2022.4.10
# Update log:
#    1.优化了程序在高dpi屏幕下的表现
#    2.修复了1900与2100年2月存在29号的问题


import json
from urllib import request,error
import random
import datetime
import ctypes
from sys import platform

import tkinter as tk
from tkinter import ttk
import tkinter.simpledialog
import tkinter.messagebox

API_HOST = "https://v1.hitokoto.cn/"
TITLE = 'Time Flies'  # 应用程序标题
TIME_ZERO = datetime.timedelta(0) #用于比较的一个常量
last_id = 10000  # 为自创的句子定义一个可用的id
is_android = platform == "linux"


def EMPTY(*arg,**kwarg):pass


def create_sentence_menu(master):
        
    sentence_menu = SentenceMenu(master)
    menu = tk.Menu(master,tearoff=False)
    menu.add_command(label="Edit",command=sentence_menu.edit)
    menu.add_command(label="Like",command=sentence_menu.like)
    menu.add_command(label="Dislike",command=sentence_menu.dislike)
    menu.add_command(label="Delete",command=sentence_menu.delete)
    menu.add_separator()
    menu.add_command(label="Create",command=sentence_menu.new)
    return menu


class SentenceHolder:
    '''Object that holds the daily sentences'''
    def __init__(self,file="sentences.txt",error="ignore"):
        try:
            f = open(file)
        except FileNotFoundError:
            if error != "ignore":raise
            else:self.sentences = {}
        
        else:
            try:
                self.sentences = json.load(f)
            except json.JSONDecodeError:
                if error != "ignore":raise
                else:self.sentences = {}
                
        self.filename = file
        
        
    @staticmethod
    def sentence_net(s_type=(),select="json"):
        '''Get a daily sentence from web.
    s_type means the type.of sentence(a to l),
    select refers to the type of response you want to get,including *text*(pure text) and *json*'''
        if not s_type:
            url = API_HOST + "?encode=" + select
        else:
            url = API_HOST + "?c=" + '&c='.join(s_type) + "&encode=" + select
        response = request.urlopen(url)
        result = response.read().decode()
        try:
            return json.loads(result)
        except json.JSONDecodeError:
            return result
            
    def add(self,sentence,id_=None,type_="原创",creator=""):
        global last_id
        if id_ is None:
            try:
                ids = [int(one) for one in self.sentences.keys()]
            except ValueError:
                id_ = last_id
                last_id += 1
            else:
                id_ = max(ids) + 1
                last_id = id_ + 1
      
        else:
            if not isinstance(id_,int):
                raise ValueError(id_+": id  must be an integer")
                
        id_ = str(id_)
        
        self.sentences[id_] = {"hitokoto":sentence,"id":id_,"type":type_,"from":creator}
        return id_
        
    def add_from_net(self,sentence):
        if sentence["type"] == "e" and (sentence["from"] is None or sentence["from"] == "原创"):
            sentence["from"] = sentence["creator"]
        self.add(sentence["hitokoto"],id_=int(sentence["id"]),creator=sentence["from"],type_=sentence["type"])
    
    def search(self,sentence):
        '''Search for the sentence in the dict'''
        result = []
        for key,value in self.sentences.items():
            if sentence == value["hitokoto"]:
                result.append(key)
        return result
                 
     
    def get_sentence(self,from_="complex"):
        '''Return a sentence,either from storage or web.'''
        if random.choice((0,1,2)) != 0 and self.sentences:
            sentence = random.choice(list(self.sentences.keys()))
            return self.sentences[sentence]
        else:
            try:
                sentence = self.sentence_net()
            except error.URLError:
                try:
                    sentence = random.choice(list(self.sentences.keys()))
                    return self.sentences[sentence]
                except IndexError:    #处理连不上网而且本地没有句子的情况
                    return {"type":"",'hitokoto':'','id':'','from':''}
            else:
                self.add_from_net(sentence)
            return sentence
        
    
    def save(self):
        self.save_as(self.filename)
    
    def save_as(self,filename):
        with open(filename,"w") as f:
            json.dump(self.sentences,f,ensure_ascii=False,indent=4)


class Target:
    def __init__(self,title,time):
        self.title = title
        self.time = time

    def __eq__(self,other):
        return (self.title,self.time) == (other.title,other.time)

    def __getitem__(self,key):
        if key == 0:
            return self.title
        elif key == 1:
            return self.time
        else:
            raise KeyError(str(key))

    def __setitem__(self,key,value):
        if key == 0:
            self.title = value
        elif key == 1:
            self.time = value
        else:
            raise KeyError(str(key))


class TargetHolder:
    '''Object that holds the targets of the time calculation'''
    def __init__(self,*targets,file='target.txt'):
        try:
            f = open(file,'r')
        except OSError:target = []
        else:
            try:
                target = json.load(f)
            except json.JsonDecodeError:
                target = []

        if not target:
            target.extend(targets)

        self.targets = []
        
        for one in target:
            try:
                one = Target(one[0],datetime.datetime.fromisoformat(one[1]))
            except TypeError:
                one = Target(one[0],one[1])
            if not one in self.targets:
                self.targets.append(one)
        
        self.filename = file

    def __getitem__(self,key):
        return self.targets[key]

    def __setitem__(self,key,value):
        self.targets[key] = value

    def __iter__(self):
        return iter(self.targets)

    def save(self):
        self.save_as(self.filename)
        

    def save_as(self,filename):
        target = []
        
        for one in self.targets:
            target.append((one.title,one.time.isoformat()))
            
        with open(filename,'w') as f:
            json.dump(target,f,ensure_ascii=False,indent=4)

    def add(self,target):
        self.targets.append(target)
        return len(self.targets)  #即为target的编号
            
    

class TimeChangeDialog(tk.simpledialog.Dialog):
    '''Dialog that help in changing an target'''
    def __init__(self,parent,target=None,mode='edit',callback=None,*arg,**kwarg):
        self.master = parent
        self.pref_time = target.time
        
        if target is None:
            self.target = Target(None,datetime.datetime.now())
        else:self.target = target
            
        self.pref_time = self.target.time
        self.title_ = tk.StringVar(value=self.target.title)
        self.year = tk.IntVar(value=self.pref_time.year)
        self.month = tk.IntVar(value=self.pref_time.month)
        self.date = tk.IntVar(value=self.pref_time.day)
        self.hour = tk.IntVar(value=self.pref_time.hour)
        self.minute = tk.IntVar(value=self.pref_time.minute)
        
        
        if callback is None:
            def callback(*arg,**kwargs):
                pass
        self._func = callback
        self._args = arg
        self._kwargs = kwarg
        
        if mode == 'edit':
            super().__init__(parent,title="修改计时时间")
        else:
            super().__init__(parent,title="创建新计时")
        
    def body(self,master):
        title = ttk.Labelframe(master,text="名称")
        ttk.Entry(title,textvariable=self.title_).pack()
        
        date = ttk.Labelframe(master,text="时间")
        date_frame = ttk.Frame(date)
        ttk.Combobox(date_frame,state="readonly",textvariable=self.year,value=list(range(1900,2101))).pack(side="left")
        ttk.Label(date_frame,text="年").pack(side="left")
        ttk.Combobox(date_frame,state='readonly',textvariable=self.month,value=list(range(1,13))).pack(side='left')
        ttk.Label(date_frame,text="月").pack(side="left")
        self.date_combobox = ttk.Combobox(date_frame,state="readonly",textvariable=self.date,postcommand=self.date_postcommand)
        self.date_combobox.pack(side="left",padx=10)
        ttk.Label(date_frame,text="日") .pack(side="left")
        
        time_frame = ttk.Frame(date)
        ttk.Combobox(time_frame,state="readonly",textvariable=self.hour,value=list(range(1,25))).pack(side="left",padx=10)
        ttk.Label(time_frame,text="时").pack(side="left")
        ttk.Combobox(time_frame,state="readonly",textvariable=self.minute,value=list(range(0,60))).pack(side="left",padx=10)
        ttk.Label(time_frame,text="分").pack(side="left")
        
        date_frame.pack()
        time_frame.pack()
        
        title.pack()
        date.pack()
        
    def date_postcommand(self):
        month = self.month.get()
        if month in (1,3,5,7,8,10,12):
            self.date_combobox["value"] = list(range(1,32))
        elif month in (4,6,9,11):
            self.date_combobox["value"] = list(range(1,31))
        else:
            year = self.year.get()
            if not year % 4:
                if year % 100:
                    self.date_combobox["value"] = list(range(1,30))
                else:
                    if not year % 400:
                        self.date_combobox["value"] = list(range(1,30))
                    else:
                        self.date_combobox["value"] = list(range(1,29))
            else:
                self.date_combobox["value"] = list(range(1,29))
            
    def apply(self):
        self.target.time = datetime.datetime(self.year.get(),self.month.get(),self.date.get(),self.hour.get(),self.minute.get())
        self.target.title = self.title_.get()
        
        self._func(*self._args,**self._kwargs)
        
    def buttonbox(self):
        box = ttk.Frame(self)

        w = ttk.Button(box, text="确定", width=10, command=self.ok, default=tk.ACTIVE)
        w.pack(side=tk.LEFT, padx=5, pady=5)
        w = ttk.Button(box, text="取消", width=10, command=self.cancel)
        w.pack(side=tk.LEFT, padx=5, pady=5)

        self.bind("<Return>", self.ok)
        self.bind("<Escape>", self.cancel)

        box.pack()



class SentenceWindow(tk.Toplevel):
    '''The dialog of editing sentences'''
    def __init__(self,master,*arg,**kwarg):
        super().__init__(master,*arg,**kwarg)
        self.master = master
        self.sentences = master.sentences
        self.menu_operation = SentenceMenu(master)
        self.title("Time Flies")
        self.main_frame = ttk.Frame(self)
        self.lb_frame = ttk.Frame(self.main_frame)
        
        self.listbox = tk.Listbox(self.lb_frame)
        self.refresh()
        
        scroll1 = ttk.Scrollbar(self.lb_frame,command=self.listbox.yview)
        scroll1.pack(side='right',fill='y')
        self.listbox.config(yscrollcommand=scroll1.set)
        
        scroll2 = ttk.Scrollbar(self.lb_frame,command=self.listbox.xview,orient='horizontal')
        scroll2.pack(side="bottom",fill="x")
        self.listbox.config(xscrollcommand=scroll2.set)

        self.listbox.bind('<Double-Button-1>',lambda event:self.edit())

        self.listbox.pack(side='left')
        self.lb_frame.pack()
        
        button_frame = ttk.Frame(self.main_frame)
        ttk.Button(button_frame,text="添加…",command=self.new).pack(side="left")
        ttk.Button(button_frame,text="编辑…",command=self.edit).pack(side="left",padx=5)
        ttk.Button(button_frame,text="删除",command=self.delete).pack(side="left",padx=5)
        button_frame.pack()
        
        self.main_frame.pack()
     
    def refresh(self):
        self.listbox.delete(0,tk.END)
        for one in self.master.sentences.sentences:
            self.listbox.insert('end',self.master.sentences.sentences[one]['hitokoto'])
         
    def new(self):
        self.menu_operation.new()
        self.refresh()
    
    def edit(self):
        s = self.sentences.search(self.listbox.get(tk.ACTIVE))[0]
        SentenceDialog(self,sentences=self.sentences,sentence_old=self.sentences.sentences[s])
        self.master.update_sentence_current()
        self.refresh()
    
    def delete(self):
        sen = self.listbox.get(tk.ACTIVE)
        id_ = self.master.sentences.search(sen)[0]
        self.menu_operation.delete(id_)
        self.listbox.delete(tk.ACTIVE)
        self.refresh()
        
        
        
class SentenceMenu:
    def __init__(self,master):
        self.master = master
        
    def edit(self):
        SentenceDialog(self.master,sentence_old=self.master.sentence_current)
        self.master.update_sentence_current()
    
    def new(self):
        return SentenceDialog(self.master)
    
    def like(self):
        pass
    
    def dislike(self):
        pass 
    
    def delete(self,sentence_id=None):
        if sentence_id is None:
            sentence_id = str(self.master.sentence_current["id"])
        del self.master.sentences.sentences[sentence_id]
        self.master.update_sentence()

             
 
class SentenceDialog(tk.simpledialog.Dialog):
    def __init__(self,master,sentences=None,sentence_old=None):
        if sentences is None:
            sentences = master.sentences
        self.sentences = sentences
        if sentence_old is None:
            self.sentence = {"id":"","hitokoto":"","type":"","from":""}
            super().__init__(master,title="创建句子")
        else:
            self.sentence = sentence_old
            super().__init__(master,title="编辑句子")

        
    def body(self,master):
        self.hitokoto = tk.StringVar(value=self.sentence["hitokoto"])
        self.creator = tk.StringVar(value=self.sentence["from"])
        self.create_myself = tk.IntVar(value=1)
        if not self.sentence["from"] or self.sentence["from"] == "我的原创":
            self.create_myself.set(0)
        
        entry_frame = ttk.Labelframe(master,text="句子")
        ttk.Entry(entry_frame,textvariable=self.hitokoto).pack()
        entry_frame.pack()
        
        creator_frame = ttk.Labelframe(master,text="作者")
        ttk.Radiobutton(creator_frame,text="原创",variable=self.create_myself,value=0,command=lambda:creator_entry.config(state="disable")).pack()
        ttk.Radiobutton(creator_frame,text="摘抄",variable=self.create_myself,value=1,command=lambda:creator_entry.config(state="normal")).pack()
        creator_entry = ttk.Entry(creator_frame,textvariable=self.creator)
        creator_entry.pack()

        if self.create_myself.get() == 0:
            creator_entry.config(state='disable')
        
        creator_frame.pack()
        
    def buttonbox(self):
        box = ttk.Frame(self)

        w = ttk.Button(box, text="确定", width=10, command=self.ok, default=tk.ACTIVE)
        w.pack(side=tk.LEFT, padx=5, pady=5)
        w = ttk.Button(box, text="取消", width=10, command=self.cancel)
        w.pack(side=tk.LEFT, padx=5, pady=5)

        self.bind("<Return>", self.ok)
        self.bind("<Escape>", self.cancel)

        box.pack()
    
    def validate(self):
        if not self.hitokoto.get():
            tk.messagebox.showwarning("提示","句子不能是空的哟")
            return 0
        if not self.creator.get() and self.create_myself.get():
            tk.messagebox.showwarning("提示","作者不能是空的~可以设为自己原创的")
            return 0
        return 1
        
    def apply(self):
        self.sentence["hitokoto"] = self.hitokoto.get()
        if not self.create_myself.get():
            author = "我的原创"
        else:
            author = self.creator.get()
        self.sentence["from"] = author
        
        if not self.sentence["id"]:
            id_ = self.sentences.add(self.sentence["hitokoto"],creator=self.sentence["from"])
            self.sentence["id"] = id_


class TargetShower(ttk.Frame):
    def __init__(self,master,target,**kwarg):
        if "relief" not in kwarg:
            kwarg["relief"] = "groove"
        super().__init__(master,borderwidth=5,**kwarg)

        self.master = master #In this file,"master" refers to an instance of "TargetRegion"
        self.master.register_shower(self)
        self.target = target
        self.title_label = tk.Label(self,text=target.title,fg='blue')
        self.time_label = tk.Label(self,text=str(target.time))

        self.title_label.pack(pady=5)
        self.time_label.pack(padx=5)

        menu = self.create_menu()
        self.bind_instance("<Button-1>",self.confirm)
        self.bind_instance("<Button-3>",lambda event:menu.post(event.x_root,event.y_root))        
        self.pack()

    def bind_instance(self,key,func):
        self.title_label.bind(key,func)
        self.time_label.bind(key,func)
        self.bind(key,func)

    def create_menu(self):
        menu = tk.Menu(self,tearoff=0)
        menu.add_command(label='编辑…',command=self.edit)
        menu.add_command(label="删除",command=self.delete)

        return menu

    def refresh(self):
        self.title_label.config(text=self.target.title)
        self.time_label.config(text=str(self.target.time))

    def edit(self):
        TimeChangeDialog(self.master,self.target,callback=self.refresh)
    
    def delete(self):
        self.destroy()
        self.master.targets.targets.remove(self.target)
        self.master.delete_shower(self)
        self.master.master.config(scrollregion=self.master.master.bbox('all')) #The master's master is an instance of tk.Canvas
        
    def confirm(self,event=None):
        self.master.targets.targets.remove(self.target)
        self.master.targets.targets.insert(0,self.target)
        self.master.change_shower_style()
        self.config(relief='sunken')


class TargetRegion(ttk.Frame):
    def __init__(self,master,targets):
        self.master = master
        self.targets = targets
        self._showers = []

        super().__init__(master)
  
    def register_shower(self,shower):
        self._showers.append(shower)
    
    def delete_shower(self,shower):
        self._showers.remove(shower)
    
    def change_shower_style(self):
        for one in self._showers:
            one.config(relief='groove')

    def create_target(self,event=None):
        new_target = Target('',datetime.datetime.now())
        TimeChangeDialog(self.master,new_target,'create',self.place_new_target,new_target)

    def place_new_target(self,target):
        TargetShower(self,target)
        self.master.config(scrollregion=self.master.bbox('all'))
        self.targets.add(target)


class App(tk.Tk):
    def __init__(self,title="毕业",finish_time=datetime.datetime(2021,6,25,16,40)):
        target = Target(title,finish_time)
        self.sentences = SentenceHolder()
        self.targets = TargetHolder(target)
        self.target = self.targets[0]
        
        super().__init__()
        # 高清修复
        if platform.startswith('win'):
            ctypes.windll.shcore.SetProcessDpiAwareness(1)
            ScaleFactor = ctypes.windll.shcore.GetScaleFactorForDevice(0)
            self.tk.call('tk', 'scaling', ScaleFactor / 72)
        
        self.title(TITLE)
        self.main_frame = ttk.Frame(self)
        ttk.Button(self.main_frame,text='那些日子',command=self.control_target_frame).pack(anchor='ne')
        self.title_label = tk.Label(self.main_frame,text="距离 "+self.target.title+" 还有",font=("华文行楷",15))
        self.title_label.pack()
        self.time_label = tk.Label(self.main_frame,font=('楷体',35),fg='red')
        self.time_label.pack()
        if is_android:
            self.time_label.bind('<Button-3>',lambda event: TimeChangeDialog(self,target=self.target))
        else:
            self.time_label.bind('<Double-Button-1>',lambda event: TimeChangeDialog(self,target=self.target))

        sentence_frame = ttk.Frame(self.main_frame)
        self.sentence_label1 = tk.Label(sentence_frame,font=('楷体',15,'bold'))
        self.sentence_label2 = tk.Label(sentence_frame)
        self.update_sentence()
        if is_android:
            self.time_label.config(font=("楷体",20))
            self.sentence_label1.config(font=("楷体",10,"bold"))
            self.sentence_label1.bind("<Double-Button-1>",lambda event:self.update_sentence())
            self.sentence_label2.bind("<Double-Button-1>",lambda event:self.update_sentence())
            self.sentence_label1.bind("<Button-3>",lambda event:SentenceWindow(self))
            self.sentence_label2.bind("<Button-3>",lambda event:SentenceWindow(self))
            menu = create_sentence_menu(self)
            self.sentence_label1.bind("<Button-1>",lambda event:menu.post(event.x-5,event.y-5))
            self.sentence_label2.bind("<Button-1>",lambda event:menu.post(event.x-5,event.y-5))
            menu.add_command(label="Sentences",command=lambda:SentenceWindow(self))
            
        else:
            self.sentence_label1.bind("<Button-1>",lambda event:self.update_sentence())
            self.sentence_label2.bind("<Button-1>",lambda event:self.update_sentence())  
            self.sentence_label1.bind("<Double-Button-1>",lambda event:SentenceWindow(self))
            self.sentence_label2.bind("<Double-Button-1>",lambda event:SentenceWindow(self))
        
            menu = create_sentence_menu(self)
            self.sentence_label1.bind("<Button-3>",lambda event:menu.post(event.x_root,event.y_root))
            self.sentence_label2.bind("<Button-3>",lambda event:menu.post(event.x_root,event.y_root))

        self.after(1,self.update_time)
        self.sentence_label1.pack()
        self.sentence_label2.pack(anchor='se')
        self.main_frame.pack(side='left')
        sentence_frame.pack(pady=30)

        self.target_frame = ttk.Frame(self)
        self.target_canvas = tk.Canvas(self.target_frame)
        self.target_frame.packd = False
        small_frame = TargetRegion(self.target_canvas,self.targets)
        self.target_canvas.bind('<Button-1>',small_frame.create_target)
        TargetShower(small_frame,target=self.targets[0],relief="sunken")
        for one in self.targets[1:]:
            TargetShower(small_frame,target=one)

        self.target_canvas.config(scrollregion=self.target_canvas.bbox('all'))

        scroll = tk.Scrollbar(self.target_frame,command=self.target_canvas.yview)
        self.target_canvas.config(yscrollcommand=scroll.set)
        self.target_canvas.pack(side='left',fill='both')
        self.target_canvas.create_window((10,50),window=small_frame,anchor='nw')
        scroll.pack(side='right',fill='y')

    def update_time(self):
        '''This method should be called every second to update time'''
        self.target = self.targets[0]
        time_delta = self.target.time - datetime.datetime.now()
        if time_delta > TIME_ZERO:
            short_time = str(time_delta).split('.')[0]
            if self.target.title:
                self.title_label.config(text="距离 "+self.target.title+" 还有") 
            else:
                self.title_label.config(text="")           
        else:
            time_delta = datetime.datetime.now() - self.target.time
            short_time = str(time_delta).split('.')[0]
            if self.target.title:
                self.title_label.config(text=self.target.title+" 已过去了")
            else:
                self.title_label.config(text="")
            
        self.title(TITLE + ' - ' + short_time)
        self.time_label.configure(text=short_time)
                    
        self.after(100,self.update_time)

    def update_sentence(self):
        self.sentence_current = self.sentences.get_sentence()
        self.update_sentence_current()

    def update_sentence_current(self):
        self.sentence_label1.config(text=self.sentence_current['hitokoto'])
        self.sentence_label2.config(text='——'+self.sentence_current['from'])

    def control_target_frame(self):
        if self.target_frame.packd:
            self.target_frame.pack_forget()
            self.target_frame.packd = False
        else:
            self.target_frame.pack(side='right',fill='both')
            self.target_frame.packd = True

        
if __name__ == '__main__':
    try:
        app = App()
        app.mainloop()
    finally:
        app.sentences.save()
        app.targets.save()
